/*
 * Copyright 2021 Confluent Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.confluent.kafka.schemaregistry.utils;

import com.google.common.base.CharMatcher;
import java.util.Objects;

/**
 * A qualified subject consists of a tenant, a context, and a subject name.
 * The qualification refers to the context and subject being qualified with a tenant.
 * The qualified subject string has the following format:
 *
 * <p>[tenant_][:.context:]subject
 *
 * <p>where tenant is optional and defaults to "default", context is optional
 * and defaults to ".", and subject is required and may be empty.
 *
 * <p>The special context name ".__GLOBAL" is used for global configuration and mode settings.
 * The special subject name "__GLOBAL" is used to store global permissions.
 * The special subject name "__EMPTY" represents an empty string.
 */
public class QualifiedSubject implements Comparable<QualifiedSubject> {

  public static final String DEFAULT_TENANT = "default";

  public static final String DEFAULT_CONTEXT = ".";

  public static final String TENANT_DELIMITER = "_";

  public static final String CONTEXT_DELIMITER = ":";

  public static final String CONTEXT_SEPARATOR = ".";

  public static final String CONTEXT_PREFIX = CONTEXT_DELIMITER + CONTEXT_SEPARATOR;

  public static final String WILDCARD = "*";

  public static final String CONTEXT_WILDCARD = CONTEXT_DELIMITER + WILDCARD + CONTEXT_DELIMITER;

  // A special context that is used for global configuration and mode settings.
  public static final String GLOBAL_CONTEXT_NAME = ".__GLOBAL";

  // Subject name under which global permissions are stored.
  public static final String GLOBAL_SUBJECT_NAME = "__GLOBAL";

  // Subject name that represents empty string
  public static final String EMPTY_SUBJECT_NAME = "__EMPTY";

  private final String tenant;
  private final String context;  // assumed to start with CONTEXT_SEPARATOR
  private final String subject;

  // visible for testing
  protected QualifiedSubject(String tenant, String qualifiedSubject) {
    String contextSubject = qualifiedSubject != null ? qualifiedSubject : "";
    if (!DEFAULT_TENANT.equals(tenant)) {
      int ix = contextSubject.indexOf(TENANT_DELIMITER);
      if (ix >= 0) {
        String tenantPrefix = contextSubject.substring(0, ix);
        contextSubject = contextSubject.substring(ix + 1);
        // set actual tenant, we only really care whether the passed in tenant was "default"
        tenant = tenantPrefix;
      } else {
        tenant = DEFAULT_TENANT;
      }
    }

    String context;
    String subject;
    if (contextSubject.startsWith(CONTEXT_PREFIX)) {
      int ix = contextSubject.substring(CONTEXT_PREFIX.length()).indexOf(CONTEXT_DELIMITER);
      if (ix >= 0) {
        ix += CONTEXT_PREFIX.length();
        context = contextSubject.substring(1, ix);  // skip first CONTEXT_DELIMITER
        subject = contextSubject.substring(ix + 1);
      } else {
        context = contextSubject.substring(1);      // skip first CONTEXT_DELIMITER
        subject = "";
      }
    } else if (contextSubject.startsWith(CONTEXT_WILDCARD)) {
      context = WILDCARD;
      subject = contextSubject.substring(CONTEXT_WILDCARD.length());
    } else {
      context = DEFAULT_CONTEXT;
      subject = contextSubject;
    }

    this.tenant = tenant;
    this.context = context;
    this.subject = subject;
  }

  public QualifiedSubject(String tenant, String context, String subject) {
    this.tenant = tenant != null ? tenant : DEFAULT_TENANT;
    this.context = context != null ? context : DEFAULT_CONTEXT;
    this.subject = subject != null ? subject : "";
  }

  public String getTenant() {
    return tenant;
  }

  public String getContext() {
    return context;
  }

  public String getSubject() {
    return subject;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    QualifiedSubject that = (QualifiedSubject) o;
    return Objects.equals(tenant, that.tenant)
        && Objects.equals(context, that.context)
        && Objects.equals(subject, that.subject);
  }

  @Override
  public int hashCode() {
    return Objects.hash(tenant, context, subject);
  }

  @Override
  public String toString() {
    return toQualifiedSubject();
  }

  public String toQualifiedContext() {
    String unqualifiedContext = toUnqualifiedContext();
    if (DEFAULT_TENANT.equals(tenant)) {
      return unqualifiedContext;
    } else {
      return tenant + TENANT_DELIMITER + unqualifiedContext;
    }
  }

  /**
   * Returns the context without the tenant prefix.
   */
  public String toUnqualifiedContext() {
    return DEFAULT_CONTEXT.equals(context)
        ? ""
        : CONTEXT_DELIMITER + context + CONTEXT_DELIMITER;
  }

  /**
   * Returns the context and subject with the tenant prefix.
   */
  public String toQualifiedSubject() {
    return toQualifiedContext() + subject;
  }

  /**
   * Returns the context and subject without the tenant prefix.
   */
  public String toUnqualifiedSubject() {
    return toUnqualifiedContext() + subject;
  }

  /**
   * Checks if the given subject is qualified with the given tenant.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return true if the subject is qualified with the tenant, false otherwise
   */
  public static boolean isQualified(String tenant, String qualifiedSubject) {
    return !DEFAULT_TENANT.equals(tenant)
        && qualifiedSubject != null
        && qualifiedSubject.startsWith(tenant + TENANT_DELIMITER);
  }

  /**
   * Creates a QualifiedSubject from the given tenant and qualified subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return the QualifiedSubject, or null if the qualified subject is null or invalid
   */
  public static QualifiedSubject create(String tenant, String qualifiedSubject) {
    try {
      return qualifiedSubject != null ? new QualifiedSubject(tenant, qualifiedSubject) : null;
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Creates a QualifiedSubject from the given tenant, context, and unqualified subject.
   *
   * @param tenant the tenant
   * @param context the context with a tenant prefix
   * @param unqualifiedSubject the subject without a tenant prefix
   * @return the QualifiedSubject, or null if the qualified subject is null or invalid
   */
  public static QualifiedSubject create(String tenant, String context, String unqualifiedSubject) {
    try {
      if (unqualifiedSubject == null) {
        return null;
      }
      if (context != null && !context.isEmpty()) {
        String unqualifiedContext = QualifiedSubject.create(tenant, context).toUnqualifiedContext();
        if (unqualifiedContext != null
            && !unqualifiedContext.isEmpty()
            && !unqualifiedSubject.startsWith(CONTEXT_PREFIX)) {
          unqualifiedSubject = QualifiedSubject.normalizeContext(unqualifiedContext)
              + unqualifiedSubject;
        }
      }
      return QualifiedSubject.createFromUnqualified(tenant, unqualifiedSubject);
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Creates a QualifiedSubject from the given tenant and unqualified subject.
   *
   * @param tenant the tenant
   * @param unqualifiedSubject the subject without a tenant prefix
   * @return the QualifiedSubject, or null if the unqualified subject is null or invalid
   */
  public static QualifiedSubject createFromUnqualified(String tenant, String unqualifiedSubject) {
    try {
      if (unqualifiedSubject == null) {
        return null;
      }
      String qualifiedSubject = DEFAULT_TENANT.equals(tenant)
          ? unqualifiedSubject
          : tenant + TENANT_DELIMITER + unqualifiedSubject;
      return new QualifiedSubject(tenant, qualifiedSubject);
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Creates a QualifiedSubject from the given tenant and unqualified context and subject.
   *
   * @param tenant the tenant
   * @param unqualifiedContext the context without a tenant prefix
   * @param unqualifiedSubject the subject without a tenant prefix
   * @return the QualifiedSubject, or null if the unqualified subject is null or invalid
   */
  public static QualifiedSubject createFromUnqualified(
      String tenant, String unqualifiedContext, String unqualifiedSubject) {
    try {
      if (unqualifiedSubject == null) {
        return null;
      }
      if (unqualifiedContext != null
          && !unqualifiedContext.isEmpty()
          && !unqualifiedSubject.startsWith(CONTEXT_PREFIX)) {
        unqualifiedSubject = QualifiedSubject.normalizeContext(unqualifiedContext)
            + unqualifiedSubject;
      }
      return QualifiedSubject.createFromUnqualified(tenant, unqualifiedSubject);
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Returns the context for the given qualified subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return the context, or DEFAULT_CONTEXT if the qualified subject is null or invalid
   */
  public static String contextFor(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    return qs != null ? qs.getContext() : DEFAULT_CONTEXT;
  }

  /**
   * Returns the qualified context for the given qualified subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return the qualified context, or empty string if the qualified subject is null or invalid
   */
  public static String qualifiedContextFor(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    return qs != null ? qs.toQualifiedContext() : "";
  }

  /**
   * Checks if the given qualified subject is a context for the given qualified subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @param qualifiedContext the context with a tenant prefix, which must not specify a subject
   * @return true if the subject is in the context, false otherwise
   */
  public static boolean isSubjectInContext(
      String tenant, String qualifiedSubject, QualifiedSubject qualifiedContext) {
    return qualifiedContext != null
        && qualifiedContext.getSubject().isEmpty() // must be a context without a subject
        && (qualifiedContext.getContext().equals(GLOBAL_CONTEXT_NAME)
        || qualifiedContext.toQualifiedContext().equals(
            QualifiedSubject.qualifiedContextFor(tenant, qualifiedSubject)));
  }

  /**
   * Qualifies the given subject (which has no tenant) with the given parent context
   * (which may have a tenant). If the parent has a context, it is used as the context
   * for the subject. If the parent has no context, the subject is placed in the default context.
   *
   * @param tenant the tenant to use if the parent has no tenant
   * @param parent the parent context, which may have a tenant
   * @param subjectWithoutTenant the subject without a tenant
   * @return the qualified subject, or null if the subjectWithoutTenant is null or invalid
   */
  public static QualifiedSubject qualifySubjectWithParent(
      String tenant, String parent, String subjectWithoutTenant) {
    return qualifySubjectWithParent(tenant, parent, subjectWithoutTenant, false);
  }

  /**
   * Qualifies the given subject (which has no tenant) with the given parent context
   * (which may have a tenant). If the parent has a context, it is used as the context
   * for the subject. If the parent has no context, the subject is placed in the default context.
   *
   * @param tenant the tenant to use if the parent has no tenant
   * @param parent the parent context, which may have a tenant
   * @param subjectWithoutTenant the subject without a tenant
   * @param prefixTenant if true, the returned qualified subject will have the tenant prefixed
   * @return the qualified subject, or null if the subjectWithoutTenant is null or invalid
   */
  public static QualifiedSubject qualifySubjectWithParent(
      String tenant, String parent, String subjectWithoutTenant, boolean prefixTenant) {
    // Since the subject has no tenant, pass the default tenant
    QualifiedSubject qualifiedSubject =
        QualifiedSubject.create(DEFAULT_TENANT, subjectWithoutTenant);
    if (qualifiedSubject == null) {
      return null;
    }
    boolean isQualified = !DEFAULT_CONTEXT.equals(qualifiedSubject.getContext());
    if (!isQualified) {
      QualifiedSubject qualifiedParent = QualifiedSubject.create(tenant, parent);
      boolean isParentQualified = qualifiedParent != null
          && !DEFAULT_CONTEXT.equals(qualifiedParent.getContext());
      if (isParentQualified) {
        // Since the subject has no tenant, pass the default tenant
        qualifiedSubject = new QualifiedSubject(
            DEFAULT_TENANT, qualifiedParent.getContext(), subjectWithoutTenant);
      }
    }
    if (prefixTenant) {
      // Prefix the tenant if prefixTenant is true.
      // For example, references are stored without tenant prefixes,
      // while alias replacements need the tenant.
      qualifiedSubject = new QualifiedSubject(
          tenant, qualifiedSubject.getContext(), qualifiedSubject.getSubject());
    }
    return qualifiedSubject;
  }

  /**
   * Normalizes the given qualified subject name.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return the normalized subject name
   */
  public static String normalize(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    return qs != null ? qs.toQualifiedSubject() : null;
  }

  /**
   * Normalizes the given context name.
   *
   * @param context the context name
   * @return the normalized context name, or null if the context is null
   * @throws IllegalArgumentException if the context name is invalid
   */
  public static String normalizeContext(String context) {
    if (context == null) {
      return null;
    }
    if (context.startsWith(CONTEXT_DELIMITER)) {
      context = context.substring(1);
    }
    if (context.endsWith(CONTEXT_DELIMITER)) {
      context = context.substring(0, context.length() - 1);
    }
    if (context.contains(CONTEXT_DELIMITER)) {
      throw new IllegalArgumentException("Context name cannot contain a colon");
    }
    if (!context.startsWith(CONTEXT_SEPARATOR)) {
      context = CONTEXT_SEPARATOR + context;
    }
    return DEFAULT_CONTEXT.equals(context) ? "" : CONTEXT_DELIMITER + context + CONTEXT_DELIMITER;
  }

  /**
   * Checks if the given qualified subject represents a context (i.e., has no subject part).
   * A context has the format ":.context:" with an empty subject part, or null for default context.
   * Also returns true for wildcard subject ("*") when tenant is not the default tenant.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return true if the qualified subject is a context, false otherwise
   */
  public static boolean isContext(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    if (qs == null || qs.getSubject().isEmpty()) {
      return true;
    }
    // Check if it's the wildcard subject in non default tenant
    return tenant != null && !DEFAULT_TENANT.equals(tenant) && WILDCARD.equals(qs.getSubject());
  }

  /**
   * Checks if the given qualified subject is the default context for the given tenant.
   * The default context is the context with name "." and an empty subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return true if the qualified subject is the default context, false otherwise
   */
  public static boolean isDefaultContext(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    return qs == null || (qs.getContext().equals(DEFAULT_CONTEXT) && qs.getSubject().isEmpty());
  }

  /**
   * Checks if the given qualified subject is the global context for the given tenant.
   * The global context is the context with name ".__GLOBAL" and an empty subject.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return true if the qualified subject is the global context, false otherwise
   */
  public static boolean isGlobalContext(String tenant, String qualifiedSubject) {
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    if (qs == null || qs.getContext() == null) {
      return false;
    }
    return qs.getContext().equals(GLOBAL_CONTEXT_NAME)
      && (qs.getSubject().isEmpty() || qs.getSubject() == null);
  }

  /**
   * Validates the given qualified subject for the given tenant.
   * A valid subject must not be null, must not contain control characters,
   * must not be "__GLOBAL" or "__EMPTY", and must not be in the global context
   * unless isConfigOrMode is true.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @return true if the qualified subject is valid, false otherwise
   */
  public static boolean isValidSubject(String tenant, String qualifiedSubject) {
    return isValidSubject(tenant, qualifiedSubject, false);
  }

  /**
   * Validates the given qualified subject for the given tenant.
   * A valid subject must not be null, must not contain control characters,
   * must not be "__GLOBAL" or "__EMPTY", and must not be in the global context
   * unless isConfigOrMode is true.
   *
   * @param tenant the tenant
   * @param qualifiedSubject the subject with a tenant prefix
   * @param isConfigOrMode true if the subject is for config or mode settings
   * @return true if the qualified subject is valid, false otherwise
   */
  public static boolean isValidSubject(
      String tenant, String qualifiedSubject, boolean isConfigOrMode) {
    if (qualifiedSubject == null || CharMatcher.javaIsoControl().matchesAnyOf(qualifiedSubject)) {
      return false;
    }
    QualifiedSubject qs = QualifiedSubject.create(tenant, qualifiedSubject);
    // For backward compatibility, we allow an empty subject
    if (qs == null || qs.getSubject().equals(GLOBAL_SUBJECT_NAME)
        || qs.getSubject().equals(EMPTY_SUBJECT_NAME)) {
      return false;
    }
    if (!isConfigOrMode && qs.getContext().equals(GLOBAL_CONTEXT_NAME)) {
      // Global context is only valid for config or mode subjects
      return false;
    }
    return true;
  }

  @Override
  public int compareTo(QualifiedSubject that) {
    if (this.getTenant() == null && that.getTenant() == null) {
      // pass
    } else if (this.getTenant() == null) {
      return -1;
    } else if (that.getTenant() == null) {
      return 1;
    } else {
      int tenantComparison = this.getTenant().compareTo(that.getTenant());
      if (tenantComparison != 0) {
        return tenantComparison < 0 ? -1 : 1;
      }
    }

    if (this.getContext() == null && that.getContext() == null) {
      // pass
    } else if (this.getContext() == null) {
      return -1;
    } else if (that.getContext() == null) {
      return 1;
    } else {
      int contextComparison = this.getContext().compareTo(that.getContext());
      if (contextComparison != 0) {
        return contextComparison < 0 ? -1 : 1;
      }
    }

    if (this.getSubject() == null && that.getSubject() == null) {
      return 0;
    } else if (this.getSubject() == null) {
      return -1;
    } else if (that.getSubject() == null) {
      return 1;
    } else {
      return this.getSubject().compareTo(that.getSubject());
    }
  }
}
